/*
 * Copyright (c) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the 'License');
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { HttpUrl } from './HttpUrl';
import { CacheControl } from './CacheControl';
import { RequestBody } from './RequestBody';
import { url } from '@kit.ArkTS';
import { HttpMethod } from './http/HttpMethod';
import { Headers } from './Headers';
import { kotlinRequire } from './utils/Utils';
import { IllegalArgumentException, IllegalStateException } from './Error/error';

export class Request {
    private _url: HttpUrl
    private _method: string
    private _headers: Headers
    private _body: RequestBody | null = null
    private _tags: Map<new (...args: Object[]) => Object, Object>
    private lazyCacheControl: CacheControl | null = null

    constructor(
        url: HttpUrl, 
        method: string, 
        headers: Headers, 
        body: RequestBody | null = null, 
        tags: Map<new (...args: Object[]) => Object, Object>
    ) {
        this._url = url
        this._method = method
        this._headers = headers
        this._body = body
        this._tags = tags
    }

    get url(): HttpUrl {
        return this._url
    }
    get method(): string {
        return this._method
    }

    get headers(): Headers {
        return this._headers
    }

    headersByName(name: string): Readonly<string[]> {
        return this._headers.values(name)
    }

    get body(): RequestBody | null {
        return this._body
    }

    get isHttps(): boolean {
        return this.url.isHttps
    }

    header(name: string): string | null {
        return this._headers.get(name)
    }

    /** Returns all headers with the given name. Same as OkHttp's headers(name) method. */
    getHeaders(name: string): readonly string[] {
        return this._headers.values(name)
    }

    /**
     * Returns the tag attached with `Object` as a key, or null if no tag is attached with
     * that key.
     *
     * Prior to OkHttp 3.11, this method never returned null if no tag was attached. Instead it
     * returned either this request, or the request upon which this request was derived with
     * [newBuilder].
     */
    tag(): Object | null
    
    /**
     * Returns the tag attached with [type] as a key, or null if no tag is attached with that key.
     * 
     * @param type - The class constructor (e.g., MyContext)
     * @returns The tag of type T, or null if not found
     */
    tag<T extends Object>(type: new (...args: Object[]) => T): T | null
    
    tag<T extends Object>(type?: new (...args: Object[]) => T): T | Object | null {
        const targetType = type ?? Object
        const value = this._tags.get(targetType as new (...args: Object[]) => Object)
        if (value === null || value === undefined) {
            return null
        }
        // Type cast the value - similar to Kotlin's type.cast(tags[type])
        if (value instanceof targetType) {
            return value as T
        }
        return value
    }

    newBuilder() {
        return new Request.Builder(this)
    }
    /**
     * Returns the cache control directives for this response. This is never null, even if this
     * response contains no `Cache-Control` header.
     */
    get cacheControl(): CacheControl {
        if (this.lazyCacheControl === null) {
            this.lazyCacheControl = CacheControl.parse(this.headers)
        }
        return this.lazyCacheControl
    }

    toString(): string {
        let result = "Request{method="
        result += this._method
        result += ", url="
        result += this._url
        
        if (this._headers.size !== 0) {
            result += ", headers=["
            let first = true
            for (const [name, value] of this._headers) {
                if (!first) {
                    result += ", "
                }
                result += name
                result += ':'
                result += value
                first = false
            }
            result += ']'
        }
        
        if (this._tags.size !== 0) {
            result += ", tags="
            // Convert Map to string representation similar to Kotlin's map.toString()
            result += '{'
            const entries: string[] = []
            for (const [type, value] of this._tags) {
                const typeName = type.name || 'Object'
                entries.push(`${typeName}=${value}`)
            }
            result += entries.join(', ')
            result += '}'
        }
        
        result += '}'
        return result
    }

    static Builder = class {
        #url: HttpUrl | null = null
        #method: string = "GET"
        #headers = new Headers.Builder()
        #body: RequestBody | null = null
        /** A mutable map of tags, or an immutable empty map if we don't have any. */
        #tags: Map<new (...args: Object[]) => Object, Object> = new Map()

        constructor(request?: Request) {
            if (request) {
                this.#url = request.url
                this.#method = request.method
                this.#body = request.body
                // Copy tags to mutable map (similar to request.tags.toMutableMap() in Kotlin)
                this.#tags = request._tags.size === 0 ? new Map() : new Map(request._tags)
                this.#headers = request.headers.newBuilder()
            }
        }

        /**
         * Sets the URL target of this request.
         *
         * @throws Error if [url] is not a valid HTTP or HTTPS URL. Avoid this
         *     exception by calling [HttpUrl.parse]; it returns null for invalid URLs.
         */
        url(value: string | HttpUrl | url.URL): this {
            if (value instanceof url.URL) {
                this.#url = HttpUrl.StringToHttpUrl(value.toString())
                return this
            }
            if (value instanceof HttpUrl) {
                this.#url = value
                return this
            }
            
            // Convert WebSocket URLs to HTTP(S)
            let finalUrl: string
            if (value.startsWith("ws:")) {
                finalUrl = "http:" + value.substring(3)
            } else if (value.startsWith("wss:")) {
                finalUrl = "https:" + value.substring(4)
            } else {
                finalUrl = value
            }
            
            this.#url = HttpUrl.StringToHttpUrl(finalUrl)
            return this
        }

        /**
         * Sets the header named [name] to [value]. If this request already has any headers
         * with that name, they are all replaced.
         */
        header(name: string, value: string): this {
            this.#headers.set(name, value)
            return this
        }

        /** Removes all headers on this builder and adds [headers]. */
        headers(headers: Headers): this {
            this.#headers = headers.newBuilder()
            return this
        }

        /**
         * Adds a header with [name] and [value]. Prefer this method for multiply-valued
         * headers like "Cookie".
         *
         * Note that for some headers including `Content-Length` and `Content-Encoding`,
         * OkHttp may replace [value] with a header derived from the request body.
         */
        addHeader(name: string, value: string): this {
            this.#headers.add(name, value)
            return this
        }

        /** Removes all headers named [name] on this builder. */
        removeHeader(name: string): this {
            this.#headers.removeAll(name)
            return this
        }

        /**
         * Sets this request's `Cache-Control` header, replacing any cache control headers already
         * present. If [cacheControl] doesn't define any directives, this clears this request's
         * cache-control headers.
         */
        cacheControl(cacheControl: CacheControl): this {
            const value = cacheControl.toString()
            if (value === "") {
                this.removeHeader("Cache-Control")
            } else {
                this.header("Cache-Control", value)
            }
            return this
        }

        get(): this {
            return this.method("GET", null)
        }

        head(): this {
            return this.method("HEAD", null)
        }

        post(body: RequestBody): this {
            return this.method("POST", body)
        }

        delete(body: RequestBody | null = null): this {
            return this.method("DELETE", body)
        }

        put(body: RequestBody): this {
            return this.method("PUT", body)
        }

        patch(body: RequestBody): this {
            return this.method("PATCH", body)
        }

        method(method: string, body: RequestBody | null): this {
            kotlinRequire(
                method !== null && method.length !== 0,
                `method cannot be null or empty`
            )
            
            if (body === null) {
                kotlinRequire(
                    !HttpMethod.requiresRequestBody(method),
                    `method ${method} must have a request body.`
                )
            } else {
                kotlinRequire(
                    HttpMethod.permitsRequestBody(method),
                    `method ${method} must not have a request body.`
                )
            }
            
            this.#method = method
            this.#body = body
            return this
        }

        /** Attaches [tag] to the request using `Object` as a key. */
        tag(tag: Object | null): this
        
        /**
         * Attaches [tag] to the request using [type] as a key. Tags can be read from a
         * request using [Request.tag]. Use null to remove any existing tag assigned for [type].
         *
         * Use this API to attach timing, debugging, or other application data to a request so that
         * you may read it in interceptors, event listeners, or callbacks.
         */
        tag<T extends Object>(type: new (...args: Object[]) => T, tag: T | null): this
        
        tag<T extends Object>(
            tagOrType: Object | null | (new (...args: Object[]) => T),
            tag?: T | null
        ): this {
            if (typeof tagOrType === 'function') {
                // Called as tag<T>(type, tag)
                const type = tagOrType as new (...args: Object[]) => T
                if (tag === null || tag === undefined) {
                    this.#tags.delete(type)
                } else {
                    if (this.#tags.size === 0) {
                        this.#tags = new Map()
                    }
                    // Force-unwrap due to lack of contracts on type casting (similar to Kotlin)
                    this.#tags.set(type, tag as Object)
                }
            } else {
                // Called as tag(tag)
                if (tagOrType === null || tagOrType === undefined) {
                    this.#tags.delete(Object)
                } else {
                    if (this.#tags.size === 0) {
                        this.#tags = new Map()
                    }
                    this.#tags.set(Object, tagOrType)
                }
            }
            return this
        }

        build(): Request {
            if (this.#url === null) {
                throw new IllegalStateException("url == null")
            }
            // Create immutable copy of tags (similar to toImmutableMap() in Kotlin)
            const immutableTags = new Map(this.#tags)
            return new Request(this.#url, this.#method, this.#headers.build(), this.#body, immutableTags)
        }

    }
}